今天主要會講 useThrottleFn 的 unit test,另外還有一個關於 setTimeout 的 bug 也會一起解決~
在 trailing 為 true、leading 為 false 會出現這個情境
const throttledFn = useThrottleFn(updateValue, 3000, true, false)
第一次觸發時,setTimeout 設定的 timeout 值是 ms - Date.now() - 0,會是一個很大的負數。
我自己是先簡單理解成,這個數字超出了 32 位整數能表示的範圍,所以發生整數溢出,溢出後的結果可能是一個非常大的正數,導致 Timer 實際上被設置為在超級久的時間,以 MDN 的說法
當使用大於 2,147,483,647 毫秒(約 24.8 天)的延遲時,這會導致整數溢位。
MDN 連結:https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
要深入研究的話,可以參考這篇:https://www.andrewdoss.dev/writing/timeouts/
知道以上造成問題的原因後,可以在 timeout 計算出來為負數的時候,就設定為 0。
timer = setTimeout(() => {
lastExec = Date.now()
resolve(invoke())
clear()
}, Math.max(0, ms - duration))
這個改動可以參考 vueuse GitHub 上的 PR:https://github.com/vueuse/vueuse/pull/2620
上面有提供案例可以試試~
先做一個 promiseTimeout function,主要是用來在測試中模擬延遲。
// src/utils/shared.js
export function promiseTimeout(ms, throwOnTimeout = false, reason = 'Timeout') {
return new Promise((resolve, reject) => {
if (throwOnTimeout)
setTimeout(() => reject(reason), ms)
else
setTimeout(resolve, ms)
})
}
// src/compositions/useThrottleFn.test.js
it('should work', async () => {
const callback = vi.fn()
const ms = 20
const run = useThrottleFn(callback, ms)
run()
run()
// 連續 run 兩次後,只會觸發一次(預設 trailing 是 false)
expect(callback).toHaveBeenCalledTimes(1)
await promiseTimeout(ms + 10)
run()
// 等待一個超過 ms 的時間再次執行,所以會成功觸發
expect(callback).toHaveBeenCalledTimes(2)
})
這段測試可以搭配 Day3 trailing 的段落會比較清楚。
// src/compositions/useThrottleFn.test.js
it('should work with trailing', async () => {
const callback = vi.fn()
const ms = 20
const run = useThrottle(callback, ms, true)
run()
run()
expect(callback).toHaveBeenCalledTimes(1)
await promiseTimeout(ms + 10)
expect(callback).toHaveBeenCalledTimes(2)
})
大致上跟上面那個案例差不多,差別在等待 ms + 10 毫秒後,因為 trailing 的關係,不需要再
執行 run,callback 就會在時間到時被呼叫第二次 。
這段測試可以搭配 Day3 leading 的段落會比較清楚。
// src/compositions/useThrottleFn.test.js
it('should work with leading', async () => {
const callback = vi.fn()
const ms = 20
const run = useThrottle(callback, ms, false, false)
run() // 因為 leading 為 false,這次不執行
run()
expect(callback).toHaveBeenCalledTimes(1)
await promiseTimeout(ms + 10)
run() // 因為 leading 為 false,這次不執行
run()
run() // 因為 trailing 為 false,這次不執行
expect(callback).toHaveBeenCalledTimes(2)
await promiseTimeout(ms + 20)
run() // 因為 leading 為 false,這次不執行
expect(callback).toHaveBeenCalledTimes(2)
})
這部分的測試在 vueuse source code 中是被歸類在 filters 的測試,所以我在專案中另開一支 filter.test.js 來實作這部分的測試。另外有一點要注意的是 throttleFilter 中的 trailing, leading 預設都是 true,這個跟 useThrottleFn 中 trailing 預設是 false 不太一樣,這部分要先改過來測試才會通過。
// src/utils/filter.js
export function throttleFilter(ms, trailing = true, leading = true, rejectOnCancel = false) { // ...略 }
// src/compositions/useThrottleFn.js
export function useThrottleFn(fn, ms, trailing = false, leading = true, rejectOnCancel = false) {
return createFilterWrapper(
throttleFilter(ms, trailing, leading, rejectOnCancel),
fn,
)
}
有兩個相關案例
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createFilterWrapper, throttleFilter } from '@/utils/filter'
describe('filters', () => {
beforeEach(() => {
vi.useFakeTimers()
})
it('should throttle', () => {
const throttledFilterSpy = vi.fn()
const filter = createFilterWrapper(throttleFilter(1000), throttledFilterSpy)
setTimeout(filter, 500) // 會觸發
setTimeout(filter, 500)
setTimeout(filter, 500)
setTimeout(filter, 500) // 會觸發(trailing)
vi.runAllTimers()
expect(throttledFilterSpy).toHaveBeenCalledTimes(2)
})
it('should throttle evenly', () => {
const debouncedFilterSpy = vi.fn()
const filter = createFilterWrapper(throttleFilter(1000), debouncedFilterSpy)
setTimeout(() => filter(1), 500)
setTimeout(() => filter(2), 1000)
setTimeout(() => filter(3), 2000)
vi.runAllTimers()
expect(debouncedFilterSpy).toHaveBeenCalledTimes(3)
expect(debouncedFilterSpy).toHaveBeenCalledWith(1)
expect(debouncedFilterSpy).toHaveBeenCalledWith(2)
expect(debouncedFilterSpy).toHaveBeenCalledWith(3)
})
})
第二個 should throttle evenly 的案例,因為卡了一下,所以寫出來做個紀錄。
0ms --- 開始
500ms --- 呼叫 filter(1) ,馬上執行 debouncedFilterSpy(1)
1000ms --- 呼叫 filter(2),因為距離上次執行還不到 1000ms,要在 1500ms 的時候才會執行
1500ms --- 執行 debouncedFilterSpy(2)
2000ms --- 執行 filter(3),因為距離上次執行還不到 1000ms,要在 2500ms 的時候才會執行
2500ms --- 執行 debouncedFilterSpy(3)
呼應到 should throttle evenly 這個案例名稱,在第一次執性 debouncedFilterSpy(1) 後,每隔 1000ms 會執行下一個,順序也正確。
GitHub:https://github.com/RhinoLee/30days_vue/pull/8/files
因為最近工作上有開始寫一些 unit test,看到原始碼這樣測 throttle 覺得很有趣,也學到了 vitest 中
useFakeTimers, runAllTimers 的用法,useThrottleFn 的 source code 也就到這邊告一段落~
明天開始會講跟畫面比較有關的 API - useParallax,轉換一下心情 XD